﻿using S7Cat.Enums;
using S7Cat.Models;
using S7Cat.Protocol;
using S7Cat.Tools;
using System.Buffers;

namespace S7Cat
{
    public sealed class Plc : BasePlc
    {
        private readonly TaskQueue _queue = new();

        public Plc(CpuType cpu, string ip, int port, short rack, short slot) : base(cpu, ip, port, rack, slot) { }

        public Plc(CpuType cpu, string ip, short rack, short slot) : base(cpu, ip, DEFAULT_PORT, rack, slot) { }

        internal override Task<MemoryOwner<byte>> RequestTsduAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
        {
            return _queue.Enqueue(base.RequestTsduAsync(data, cancellationToken));
        }

        /// <summary>
        /// 使用内存池缓存，读取字节数组
        /// </summary>
        /// <param name="address"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        internal override async Task<MemoryOwner<byte>> ReadBytesPooledAsync(DataItemAddress address, CancellationToken cancellationToken = default)
        {
            var maxReadSize = MaxPDU - 18;
            var buffer = MemoryOwner<byte>.Allocate(address.ByteCount);
            if (address.ByteCount > maxReadSize)
            {
                var offset = 0;

                while (offset < address.ByteCount)
                {
                    var maxRead = Math.Min(address.ByteCount - offset, maxReadSize);
                    await ReadBytesSplitAsync(address.Split(offset, maxRead), buffer.Slice(offset, maxRead).Memory, cancellationToken).ConfigureAwait(false);
                    offset += maxRead;
                }
            }
            else
            {
                await ReadBytesSplitAsync(address, buffer.Memory, cancellationToken).ConfigureAwait(false);
            }

            return buffer;
        }

        /// <summary>
        /// 读取字节数组
        /// </summary>
        /// <param name="address"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override async Task<byte[]> ReadBytesAsync(DataItemAddress address, CancellationToken cancellationToken = default)
        {
            var maxReadSize = MaxPDU - 18;
            var buffer = new byte[address.ByteCount];
            if (address.ByteCount > maxReadSize)
            {
                var offset = 0;

                while (offset < address.ByteCount)
                {
                    var maxRead = Math.Min(address.ByteCount - offset, maxReadSize);
                    await ReadBytesSplitAsync(address.Split(offset, maxRead), buffer.AsMemory(offset, maxRead), cancellationToken).ConfigureAwait(false);
                    offset += maxRead;
                }
            }
            else
            {
                await ReadBytesSplitAsync(address, buffer, cancellationToken).ConfigureAwait(false);
            }

            return buffer;
        }

        /// <summary>
        /// 批量读取
        /// </summary>
        /// <param name="addresses"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        internal override async Task<byte[]> ReadMultipleAsync(Memory<DataItemAddress> addresses, CancellationToken cancellationToken = default)
        {
            var ranges = ArrayPool<Range>.Shared.Rent((int)Math.Sqrt(addresses.Length));
            try
            {
                var count = ConnectionRequest.SplitReadRequest(addresses.Span, ranges, MaxPDU);

                if (count == 0)
                {
                    throw new Exception("splited requests is empty.");
                }

                var totalSize = GetByteCount(addresses.Span);
                var buffer = new byte[totalSize];

                var offset = 0;

                for (int i = 0; i < count; i++)
                {
                    var splitAdr = addresses.Span[ranges[i]];
                    var size = GetByteCount(splitAdr);
                    using var request = ConnectionRequest.BuildSplitReadRequest(splitAdr);
                    ConnectionRequest.ThrowIfRequestSizeTooLong(request.WrittenCount, MaxPDU);

                    using var data = await RequestTsduAsync(request.WrittenMemory, cancellationToken).ConfigureAwait(false);

                    var span = data.Span;

                    if (span.Length < 15)
                    {
                        throw new Exception($"response error, request data : {Convert.ToHexString(span)}");
                    }

                    PackageParser.ValidateResponseCode((ReadWriteErrorCode)span[14]);

                    PackageParser.FillReadResponses(span, addresses.Span[ranges[i]], buffer.AsSpan(offset, size));

                    offset += size;
                }

                return buffer;
            }
            finally
            {
                ArrayPool<Range>.Shared.Return(ranges);
            }
        }

        /// <summary>
        /// 批量读取
        /// </summary>
        /// <param name="addresses"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public override async Task<MultipleReader> ReadMultipleReaderAsync(Memory<DataItemAddress> addresses, CancellationToken cancellationToken = default)
        {
            var ranges = ArrayPool<Range>.Shared.Rent((int)Math.Sqrt(addresses.Length));
            try
            {
                var count = ConnectionRequest.SplitReadRequest(addresses.Span, ranges, MaxPDU);

                if (count == 0)
                {
                    throw new Exception("splited requests is empty.");
                }

                var totalSize = GetByteCount(addresses.Span);
                var buffer = ArrayPool<byte>.Shared.Rent(totalSize);

                var offset = 0;

                for (int i = 0; i < count; i++)
                {
                    var splitAdr = addresses.Span[ranges[i]];
                    var size = GetByteCount(splitAdr);
                    using var request = ConnectionRequest.BuildSplitReadRequest(splitAdr);
                    ConnectionRequest.ThrowIfRequestSizeTooLong(request.WrittenCount, MaxPDU);

                    using var data = await RequestTsduAsync(request.WrittenMemory, cancellationToken).ConfigureAwait(false);

                    var span = data.Span;

                    if (span.Length < 15)
                    {
                        throw new Exception($"response error, request data : {Convert.ToHexString(span)}");
                    }

                    PackageParser.ValidateResponseCode((ReadWriteErrorCode)span[14]);

                    PackageParser.FillReadResponses(span, addresses.Span[ranges[i]], buffer.AsSpan(offset, size));

                    offset += size;
                }

                return new MultipleReader(addresses.Span, buffer, true);
            }
            finally
            {
                ArrayPool<Range>.Shared.Return(ranges);
            }
        }

        /// <summary>
        /// 根据地址写入字节数组
        /// </summary>
        /// <param name="address"></param>
        /// <param name="bytes"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override async Task WriteBytesAsync(DataItemAddress address, Memory<byte> bytes, CancellationToken cancellationToken = default)
        {
            var maxWrite = MaxPDU - 35;
            if (bytes.Length > maxWrite)
            {
                var write = 0;
                while (write < bytes.Length)
                {
                    var max = Math.Min(bytes.Length - write, maxWrite);
                    await WriteBytesSplitAsync(address.Split(write, max), bytes.Slice(write, max), cancellationToken).ConfigureAwait(false);
                    write += max;
                }
            }
            else
            {
                await WriteBytesSplitAsync(address, bytes, cancellationToken).ConfigureAwait(false);
            }
        }

        /// <summary>
        /// 批量写入
        /// </summary>
        /// <param name="addresses"></param>
        /// <param name="dataBytes"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public override async Task WriteMultipleAsync(Memory<DataItemAddress> addresses, Memory<byte> dataBytes, CancellationToken cancellationToken = default)
        {
            var ranges = ArrayPool<Range>.Shared.Rent((int)Math.Sqrt(addresses.Length));
            try
            {
                var count = ConnectionRequest.SplitWriteRequest(addresses.Span, ranges, MaxPDU);

                if (count == 0)
                {
                    throw new Exception("splited requests is empty.");
                }

                var offset = 0;
                for (int i = 0; i < count; i++)
                {
                    var splitAdr = addresses.Span[ranges[i]];
                    var size = GetByteCount(splitAdr);
                    using var request = ConnectionRequest.BuildSplitWriteRequest(splitAdr, dataBytes.Span.Slice(offset, size));
                    ConnectionRequest.ThrowIfRequestSizeTooLong(request.WrittenCount, MaxPDU);

                    using var data = await RequestTsduAsync(request.WrittenMemory, cancellationToken).ConfigureAwait(false);

                    var result = data.Span;
                    PackageParser.ValidateWriteResponses(result, addresses.Span[ranges[i]]);
                    offset += size;
                }
            }
            finally
            {
                ArrayPool<Range>.Shared.Return(ranges);
            }
        }
    }
}
